"""
Cкрипт со статистикой чтния книг.

Author: Milinuri Nirvalen
Ver: 2.3.2

date: Получение текущего дня в году
json: Управление файлами с данными
Path: Проверка существования файла данных
"""

import argparse
import json

from datetime import date
from pathlib import Path
from typing import Optional


r = "\033[0m"
g = "\033[90m"

# Путь к файлу с данными о книгах
data_path = Path("books.json")

# Цвета палитры в зависимости от прогресса
color_model = {0: "\033[35m", 1: "\033[34m",
               4: "\033[36m", 5: "\033[32m",
               7: "\033[33m", 9: "\033[31m"}

# Неполные блоков для графика и линии прогресса
graph_model = {1:"▁", 3:"▂", 4:"▃", 5:"▄", 6:"▅", 8:"▆", 9:"▇"}
progress_model = {1:"▏", 3:"▎", 4:"▍", 5:"▌", 6: "▋",7:"▋", 8:"▊", 9:"▉"}


"""
Параметры книг по умолчанию
===========================
    pages: Кол-во страниц в книге
    days_left: Сколько дней осталось на чтение книги (30)
    days: Список номерами текущей страницы в каждый день
    today: Сегодняшнего день в году
"""
model = {"pages":100, "days_left":30, "days":[0], "today":0}


def save_file(data: dict) -> dict:
    """Записывает данные о книгах в json-файл.

    Args:
        data (dict): Словарь с данными для записи

    Returns:
        dict: Записанные в файл данные
    """
    with open(data_path, 'w') as f:
        f.write(json.dumps(data, indent=4))
        return data

def load_file() -> dict:
    """Загружает json файл с данными о книгах.
    Если файла нет - создаёт его.

    Returns:
        dict: Инфопрмация о книгах
    """
    if not data_path.is_file():
        example_book = {
                "pages": 273,
                "days_left": 18,
                "days": [14, 27, 48, 53, 72],
                "today": 0
        }

        return save_file({"example":example_book})

    with open(data_path) as f:
        return json.loads(f.read())


# Вспомогательные функции
# =======================

def ask_user(text: str, default: Optional[str] = None) -> str:
    text = f"\033[90m[?] {text} (\033[33m{default}\033[90m)\033[0m "
    return input(text) or default

def value_by_nearest_key(n: int, model: Optional[dict] = color_model) -> str:
    return model[min(model, key=lambda x: abs(x - n))]

def graph(data: list, size: Optional[int] = 10) -> None:
    """Отрисовывает цветной текстовый график.

    Args:
        data (list): Последовательность значений для графика
        size (int, optional): Высота графика (10)
    """
    # Нормальизуем значения в промежуток от 0.0 до size (10.0)
    md = max(data)
    pr = ([0] * (60 - len(data))) + [round(x/md*size, 1) for x in data[-60:]]

    # Пробегаемся по каждой строке (10 -> 0)
    for x in range(size, 0, -1):
        colored = False
        i = int(round(md/size*x, 2))
        line = f'{i:4}|'

        # Пробегаемся слева направо по строке
        for j in pr:
            if j >= x-1 and not colored:
                line += value_by_nearest_key(size-x)
                colored = True

            if j >= x:
                line += f'█'

            elif j >= x-1:
                line += value_by_nearest_key(int(j % 1 * 10), graph_model)

            else:
                line += '_'

        print(f'{line}\033[0m')

def progress_line(i: int, total: int, size: Optional[int] = 30) -> str:
    """Возвращает строковую линию прогресса.

    Args:
        i (int): Текущее значение
        total (int): Максимальное значение
        size (int, optional): Размер линии прогресса

    Returns:
        str: Линия прогресса
    """
    pr = round(i/total*size, 1)
    lpr = int(pr)
    head_pr = int(pr % 1 * 10)
    color = value_by_nearest_key(10-(i/total*10))
    head = value_by_nearest_key(head_pr, progress_model) if pr <= size else ""
    fpr = round(i/total*100, 2)
    return f" {color}{'█'*(lpr)}{head}{' '*(size-lpr-1)}\033[0m: {fpr}%"

def procent_diff(a: int, b: int) -> str:
    color = "\033[31m" if a >= b else "\033[32m"
    return f"({color}{abs(round((b/a)*100, 2))}%\033[0m)"


# Вспомогтельный класс
# ====================

class BookStats:
    """Класс описывает статистику чтения книги.

    Attributes:
        ars (float): Средняя скорость чтения книги (страниц/день)
        cp (int): Текущая страница
        data (dict): Данные о прочтении книги
        name (str): Ключевое название книги
        pages_left (int): Сколько страниц осталось читать
        pr (float): Сколько процентов книги уже прочитано
        pr_up (float): Сколько процентов книги вы прочитали за сегодня
        read_p (float): Сколько прочитано страниц в каждый из дней
        rt (int): Сколько страниц вы прочитали за сегодня
        tp (int): Сколько страни нужно прочитать за этот день
    """

    def __init__(self, name: str, data: dict) -> None:
        super(BookStats, self).__init__()
        self.name = name
        self.data = data

        # Добавляет атрибуты: pages, days_left... из data
        self.__dict__.update(self.data)

        self._ars = None
        self.read_p = [self.days[x] - self.days[x-1] for x in range(1, len(self.days))]
        self.rt = self.read_p[-1]
        self.cp = self.days[-1]
        self.pages_left = self.pages - self.cp
        self.pdays_left = round(self.pages_left/self.ars, 2)
        self.tp = round(self.pages_left/self.days_left, 2)
        self.pr_up = round(self.rt/self.pages*100, 2)
        self.pr = round(self.cp/self.pages*100, 2)


    @property
    def ars(self) -> float:
        """Получает среднию скорость чтения за последнии 7 дней.

        Returns:
            float: Среднее страниц в день
        """
        if self._ars is None:
            i = min(len(self.read_p), 7)
            total_pages = sum(list(filter(lambda x: x > 3, self.read_p))[-7:])
            self._ars = round(total_pages / i, 2)
        return self._ars or 1

    def get_diff(self, a: int, b: int) -> str:
        """Получает разницу между числами А и B.
        Сколько прочитано страниц за вчера и сегодня.

        Args:
            a (int): Первое число для сравнения
            b (int): Второе число для сравнения

        Returns:
            str: Строковая раззница между числами
        """
        diff = f"{a-b} меньше" if a >= b else f"{b-a} больше"
        pr_diff = f" ({abs(round(b/a*100, 2))}%)" if a > 0 else ""
        return f"Прочитано на {diff}, чем вчера{pr_diff}"

    def status(self) -> None:
        """Краткая сводку о чтении книги."""
        c = value_by_nearest_key(10-round(self.cp/self.pages*10))
        text = f"{c}{self.days_left} {g}[{c}{self.pr}%{g}]{r} {self.name} "
        text += f"{self.cp}/{self.pages} ({self.pages_left})"

        readed = self.read_p[-1]
        if readed:
            readed_pr = round(readed/self.pages*100, 2)
            text += f" {c}+{readed} стр. +{readed_pr}%{r}"

        print(text)


# Основные функции
# ================

def get_previous_book(book: BookStats) -> BookStats:
    """Получет статистику книги за вчерашний день.

    Args:
        book (BookStats): Экзмеляр BookStats за сегодня

    Returns:
        BookStats: Экземпляр BookStats за вчерашний день
    """
    data = book.data.copy()
    data["days_left"] += 1
    data["days"] = data["days"][:-1] or [0]
    data["today"] -= 1
    return BookStats(book.name, data)

def read_info(book: BookStats) -> None:
    """Отображение статистики о прочтении книги."""
    print(progress_line(book.cp, book.pages), "Прочитано")
    if book.rt < book.tp:
        print(progress_line(book.rt, book.tp), "Цель на сегодня")

    if len(book.days) > 3:
        p_book = get_previous_book(book)
        ars_diff = f" {procent_diff(p_book.ars, book.ars)}"
        days_diff = f" {procent_diff(book.pdays_left, p_book.pdays_left)}"
        target_diff = f" {procent_diff(book.tp, p_book.tp)}"
    else:
        ars_diff = ""
        days_diff = ""
        target_diff = ""

    print(f"  Читаете {len(book.days)} дней по ~{book.ars} страниц{ars_diff}")
    print(f"  Закончите за ~{book.pdays_left} дней{days_diff}")
    print(f"  Осталось {book.days_left} дней по {book.tp} в день{target_diff}")
    print(f"  Cегодня {book.rt} страниц, это +{book.pr_up}%")

    if len(book.days) > 3:
        print(book.get_diff(book.read_p[-2], book.read_p[-1]))
    graph(book.read_p)


def books_list(books: list[BookStats]) -> None:
    """Отображает список книг, которые вы читаете.

    Args:
        books (list): Список из экземпляров BookStats
    """
    def sort_func(x):
        return x.pages - x.days[-1]

    if len(books):
        for x in sorted(books, key=sort_func):
            x.status()
    else:
        print('Вы не читаете ни одной книги :(')

def edit_data(data: dict, days_shift: Optional[bool] = False) -> dict:
    """Обновляет данные о читаемой книги.
    Запрашивает кол-во страниц и текущую страницу.

    Args:
        data (dict): Данные о книге
        days_shift (bool, optional): Принудительная смена дня

    Returns:
        dict: Изменённые данные книги
    """
    today = int(date.today().strftime('%j'))
    a = ask_user("Страниц в книге", str(data["pages"]))

    # Если пользователь указал 0 - удаляем книгу
    if a == "0":
        return None

    # Перерасчитываем статистику, работет в обоих направлениях
    elif a and a[0] == '*' and a[1:].isdigit():
        a = int(a[1:])
        data['days'] = list(map(lambda x: round(a/data['pages']*x),
                                data['days']))
        data['pages'] = a

    # Если количество страниц изменилось, сбрасываем информацию
    elif a.isdigit() and int(a) != data["pages"]:
        data = model
        data['pages'] = int(a)

        # Спрашиваем о кол-ве дней, оставшихся на чтение книги
        a = int(ask_user('Осталось на чтение', data["days_left"]))
        data["days_lefta"] = a

    # Ежедневный сдвиг значения прочтения
    if today != data['today'] or days_shift:
        if len(data['days']):
            data['days'].append(data['days'][-1])
        else:
            data['days'].append(0)
        data['today'] = today
        data['days_left'] -= 1

    # Если время на чтение книги истекло - удаляем книгу.
    if data["days_left"] <= 0:
        return None

    # Изменение текущей страницы
    a = ask_user('Вы на странице', str(data["days"][-1]))
    if a.isdigit():
        data['days'][-1] = int(a)

    return data

def main() -> None:
    parser = argparse.ArgumentParser("Статиска чтения книг")
    parser.add_argument("-e", "--edit", default=False, action="store_true",
                        help="Изменить информацию о книге")
    parser.add_argument("-d", "--day-shift", default=False, action="store_true",
                        help="Симулировать смену дня")
    parser.add_argument("book", nargs="*", help="Ключ книги")

    args = parser.parse_args()
    data = load_file()
    books = {k: BookStats(k, v) for k, v in data.items()}

    edit_mode = False

    for x in args.book:
        if args.edit:
            edited = edit_data(data.get(x, model), args.day_shift)

            if not edited and data.get(x):
                del data[x]
                del books[x]
                print(x, 'Удалена.')
                continue

            data[x] = edited
            books[x] = BookStats(x, edited)

        if x not in data:
            print(x, 'Не существует.')
            continue

        read_info(books[x])

    books_list(books.values())
    save_file(data)


# запуск скрипта
# ==============

if __name__ == '__main__':
    main()
